Evaluación de la Calidad de los Datos

Análisis Exploratorio para Regresión (knee_flex_deg) y Clasificación (Risk_Lesion)

ASIM
Autor/a

Ph.D. Pablo Eduardo Caicedo Rodríguez

Fecha de publicación

25 de octubre de 2025

# install.packages(c("DiagrammeR", "reticulate", "kableExtra", "tidyverse", "knitr", "cowplot", "ggfx"))
library("DiagrammeR")
library("reticulate")
library("kableExtra")
library("tidyverse")
library("knitr")
library("cowplot")
library("ggfx")
knitr::opts_chunk$set(echo = FALSE)

def.chunk.hook <- knitr::knit_hooks$get("chunk")
knitr::knit_hooks$set(chunk = function(x, options) {
    x <- def.chunk.hook(x, options)
    ifelse(options$size != "normalsize", paste0("\n \\", options$size, "\n\n", x, "\n\n \\normalsize"), x)
})
import numpy as np
import matplotlib.pyplot as plt
path_ecg="../../data"

#https://developer.nvidia.com/embedded/learn/get-started-jetson-nano-devkit#write

Introducción

La evaluación de la calidad de los datos es una etapa crítica del análisis exploratorio, pues condiciona la validez estadística y la interpretabilidad de los modelos. En este documento se operacionalizan criterios y procedimientos para:

  • Regresión: knee_flex_deg (continua).
  • Clasificación: Risk_Lesion (categórica).
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 6 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Knee_Flex_deg      1000 non-null   float64
 1   EMG_Quad_RMS_mV    1000 non-null   float64
 2   EMG_Ham_RMS_mV     1000 non-null   float64
 3   GRF_Vert_Norm_BW   1000 non-null   float64
 4   Omega_Shank_deg_s  1000 non-null   float64
 5   Hip_Flex_deg       1000 non-null   float64
dtypes: float64(6)
memory usage: 47.0 KB

1. Identificación de valores faltantes

Se cuantifican porcentajes de celdas vacías y se priorizan acciones: eliminar (>60%), imputar (30–60%), o evaluar impacto (<30%).

missing_tbl = (df.isna().mean().sort_values(ascending=False)*100.0).round(2).to_frame("Porc_Faltantes_%")
missing_tbl.head(20)
                   Porc_Faltantes_%
Knee_Flex_deg                   0.0
EMG_Quad_RMS_mV                 0.0
EMG_Ham_RMS_mV                  0.0
GRF_Vert_Norm_BW                0.0
Omega_Shank_deg_s               0.0
Hip_Flex_deg                    0.0

Visualización de valores faltantes por variable

import matplotlib.pyplot as plt

plt.figure(figsize=(10, max(3, len(missing_tbl)*0.25)))
vals = missing_tbl["Porc_Faltantes_%"].values
labs = missing_tbl.index.values
ypos = np.arange(len(labs))
plt.barh(ypos, vals)
plt.yticks(ypos, labs)
([<matplotlib.axis.YTick object at 0x715446a316a0>, <matplotlib.axis.YTick object at 0x7154469ce0d0>, <matplotlib.axis.YTick object at 0x715446a3df90>, <matplotlib.axis.YTick object at 0x715446a3e710>, <matplotlib.axis.YTick object at 0x715446a3ee90>, <matplotlib.axis.YTick object at 0x715446a3f610>], [Text(0, 0, 'Knee_Flex_deg'), Text(0, 1, 'EMG_Quad_RMS_mV'), Text(0, 2, 'EMG_Ham_RMS_mV'), Text(0, 3, 'GRF_Vert_Norm_BW'), Text(0, 4, 'Omega_Shank_deg_s'), Text(0, 5, 'Hip_Flex_deg')])
plt.xlabel("% de celdas faltantes")
plt.title("Porcentaje de valores faltantes por variable")
plt.tight_layout()
plt.show()


2. Detección de cardinalidad irregular

Se inspecciona el número de valores únicos por variable para detectar: cardinalidad 1 (sin información), codificaciones erróneas o cardinalidad excesiva.

card_tbl = df.nunique(dropna=False).sort_values(ascending=True).to_frame("Cardinalidad")
card_tbl.head(20)
                   Cardinalidad
Knee_Flex_deg               772
EMG_Quad_RMS_mV            1000
EMG_Ham_RMS_mV             1000
GRF_Vert_Norm_BW           1000
Omega_Shank_deg_s          1000
Hip_Flex_deg               1000

Distribución de cardinalidad (todas las columnas)

plt.figure(figsize=(10, max(3, len(card_tbl)*0.25)))
vals = card_tbl["Cardinalidad"].values
labs = card_tbl.index.values
ypos = np.arange(len(labs))
plt.barh(ypos, vals)
plt.yticks(ypos, labs)
([<matplotlib.axis.YTick object at 0x7154468e1e50>, <matplotlib.axis.YTick object at 0x7154469182d0>, <matplotlib.axis.YTick object at 0x715446918a50>, <matplotlib.axis.YTick object at 0x7154469191d0>, <matplotlib.axis.YTick object at 0x715446919950>, <matplotlib.axis.YTick object at 0x71544691a0d0>], [Text(0, 0, 'Knee_Flex_deg'), Text(0, 1, 'EMG_Quad_RMS_mV'), Text(0, 2, 'EMG_Ham_RMS_mV'), Text(0, 3, 'GRF_Vert_Norm_BW'), Text(0, 4, 'Omega_Shank_deg_s'), Text(0, 5, 'Hip_Flex_deg')])
plt.xlabel("Número de valores únicos")
plt.title("Cardinalidad por variable")
plt.tight_layout()
plt.show()


3. Detección de valores atípicos (Outliers)

Se consideran dos enfoques: IQR de Tukey y z-score.

from typing import Tuple, Dict

def outlier_bounds_iqr(series: pd.Series, k: float = 1.5) -> Tuple[float, float]:
    s = series.dropna().astype(float)
    q1 = np.percentile(s, 25)
    q3 = np.percentile(s, 75)
    iqr = q3 - q1
    lo = q1 - k*iqr
    hi = q3 + k*iqr
    return lo, hi

def outlier_summary(df_num: pd.DataFrame, method: str = "iqr", k: float = 1.5, z: float = 3.0) -> pd.DataFrame:
    rows = []
    for col in df_num.columns:
        s = df_num[col].dropna().astype(float)
        if s.empty:
            continue
        if method == "iqr":
            lo, hi = outlier_bounds_iqr(s, k=k)
            out = ((df_num[col] < lo) | (df_num[col] > hi)).sum()
            rows.append((col, lo, hi, int(out)))
        else:
            m = s.mean(); sd = s.std(ddof=0)
            if sd == 0:
                rows.append((col, np.nan, np.nan, 0))
            else:
                out = ((np.abs((df_num[col]-m)/sd)) > z).sum()
                rows.append((col, m - z*sd, m + z*sd, int(out)))
    return pd.DataFrame(rows, columns=["Variable", "Límite_inferior", "Límite_superior", "Conteo_outliers"])

df_num = df.select_dtypes(include=[np.number])
iqr_tbl = outlier_summary(df_num, method="iqr", k=1.5).sort_values("Conteo_outliers", ascending=False)
z_tbl   = outlier_summary(df_num, method="z",   z=3.0).sort_values("Conteo_outliers", ascending=False)

iqr_tbl.head(15), z_tbl.head(15)
(            Variable  Límite_inferior  Límite_superior  Conteo_outliers
0      Knee_Flex_deg      -101.289727       164.359342                0
1    EMG_Quad_RMS_mV        -0.101106         0.366573                0
2     EMG_Ham_RMS_mV        -0.092222         0.302515                0
3   GRF_Vert_Norm_BW        -0.630861         1.957452                0
4  Omega_Shank_deg_s      -801.247432       784.800697                0
5       Hip_Flex_deg       -34.877758        64.126131                0,             Variable  Límite_inferior  Límite_superior  Conteo_outliers
0      Knee_Flex_deg       -74.265149       147.551616                0
1    EMG_Quad_RMS_mV        -0.068715         0.334233                0
2     EMG_Ham_RMS_mV        -0.060132         0.272798                0
3   GRF_Vert_Norm_BW        -0.479935         1.786190                0
4  Omega_Shank_deg_s      -694.930515       679.530591                0
5       Hip_Flex_deg       -28.294709        57.705241                0)

Boxplots univariados seleccionados (matplotlib)

cols_plot = [c for c in df_num.columns if c.lower() in ["knee_flex_deg", "bodymass", "height"]]
if not cols_plot:
    # Selección automática de hasta 3 numéricas
    cols_plot = df_num.columns.tolist()[:3]

fig, axes = plt.subplots(nrows=len(cols_plot), ncols=1, figsize=(9, 4*len(cols_plot)))
if len(cols_plot) == 1:
    axes = [axes]

for ax, col in zip(axes, cols_plot):
    ax.boxplot(df_num[col].dropna().astype(float), vert=True, showmeans=True)
    ax.set_title(f"Boxplot: {col}")
    ax.set_ylabel(col)

plt.tight_layout()
plt.show()


4. Validez contextual (reglas de plausibilidad)

Se definen reglas de plausibilidad fisiológica/experimental y se generan banderas de validación.

flags = {}
if "knee_flex_deg" in df.columns:
    s = pd.to_numeric(df["knee_flex_deg"], errors="coerce")
    flags["knee_flex_deg_out_of_range"] = ~s.between(0, 180)  # rango articular plausible (ajuste si aplica)

if "bodymass" in df.columns:
    s = pd.to_numeric(df["bodymass"], errors="coerce")
    flags["bodymass_out_of_range"] = ~s.between(30, 300)      # ejemplo amplio; ajuste según protocolo

val_flags = pd.DataFrame(flags) if flags else pd.DataFrame()
val_flags.sum() if not val_flags.empty else "No se definieron reglas de plausibilidad para las variables presentes."
'No se definieron reglas de plausibilidad para las variables presentes.'

5. Análisis de correlación (soporte a decisiones)

Para orientar transformaciones o selección de variables se explora la correlación entre numéricas.

corr = df_num.corr(numeric_only=True)
corr.round(3).iloc[:8, :8]  # vista parcial
                   Knee_Flex_deg  ...  Hip_Flex_deg
Knee_Flex_deg              1.000  ...         0.178
EMG_Quad_RMS_mV            0.028  ...         0.035
EMG_Ham_RMS_mV            -0.063  ...         0.033
GRF_Vert_Norm_BW           0.087  ...        -0.009
Omega_Shank_deg_s          0.940  ...        -0.045
Hip_Flex_deg               0.178  ...         1.000

[6 rows x 6 columns]

Mapa de calor de correlaciones (matplotlib, sin seaborn)

plt.figure(figsize=(8,6))
im = plt.imshow(corr, interpolation='nearest', aspect='auto')
_ = plt.colorbar(im, fraction=0.046, pad=0.04)
_ = plt.xticks(ticks=np.arange(len(corr.columns)), labels=corr.columns, rotation=90)
_ = plt.yticks(ticks=np.arange(len(corr.index)), labels=corr.index)
_ = plt.title("Matriz de correlación (numéricas)")
_ = plt.tight_layout()
plt.show()


6. Estructura de clases para Risk_Lesion (si aplica)

Se inspecciona balance de clases y su impacto potencial en métricas y validación.

if "Risk_Lesion" in df.columns:
    vc = df["Risk_Lesion"].value_counts(dropna=False)
    vcp = df["Risk_Lesion"].value_counts(normalize=True, dropna=False).mul(100).round(2)
    display_tbl = pd.DataFrame({"Conteo": vc, "Porcentaje_%": vcp})
    display_tbl
else:
    "La variable 'Risk_Lesion' no está presente en el dataset."
"La variable 'Risk_Lesion' no está presente en el dataset."

Barras de distribución de clases (matplotlib)


7. Plan de Calidad de Datos (plantilla)

Use la siguiente plantilla para documentar problemas detectados y acciones.

Variable Problema detectado Evidencia/Regla Acción propuesta Responsable Fecha
knee_flex_deg Outliers (>p99) IQR/Z-score Validar en ficha clínica / Winsorizar Equipo clínico AAAA-MM-DD
Risk_Lesion Desequilibrio Conteo de clases Muestreo estratificado / Umbrales Equipo DS AAAA-MM-DD
bodymass Faltantes 8% % faltantes Imputación mediana Equipo DS AAAA-MM-DD
Dominancia Codificación heterogénea Cardinalidad/etiquetas Normalizar etiquetas Equipo DS AAAA-MM-DD

8. Recomendaciones para el modelado posterior

  • Regresión (knee_flex_deg): evaluar transformaciones (log/sqrt) si hay asimetría marcada; controlar outliers (winsorización o RobustScaler).
  • Clasificación (Risk_Lesion): balancear clases si hay desproporción; definir métrica principal (AUC/F1) y validación estratificada.